// @ts-nocheck
/**
 * @license
 * Copyright 2021 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview Bitmap input field.
 * @author gregoryc@outlook.com (Greg Cannon)
 */


import Blockly from 'blockly/core';

const DEFAULT_HEIGHT = 5;
const DEFAULT_WIDTH = 5;
const PIXEL_SIZE = 15;
const FILLED_PIXEL_COLOR = '#363d80';
const EMPTY_PIXEL_COLOR = 'white';
const BG_COLOR = '#e5e7f1';

/**
 * Field for inputting a small bitmap image.
 * Includes a grid of clickable pixels that's exported as a bitmap.
 */
class FieldBitmap extends Blockly.Field {
  /**
   *Constructor for the bitmap field.
   * @param {!Array<!Array<number>>=} value 2D rectangular array of 1s and 0s.
   * @param {Function=} validator A function that is called to validate.
   * @param {!Object=} config Config A map of options used to
   * configure the field.
   */
  constructor(value = undefined, validator = undefined, config = undefined) {
    super(value, validator, config);
    this.SERIALIZABLE = true;
    // Configure value, height, and width
    if (this.getValue() !== null) {
      this.imgHeight_ = this.getValue().length;
      this.imgWidth_ = this.getValue()[0].length || 0;
    } else {
      this.imgHeight_ = (config && config['height']) || DEFAULT_HEIGHT;
      this.imgWidth_ = (config && config['width']) || DEFAULT_WIDTH;
    }

    // this.SERIALIZABLE = true;

    this.filledPixelColor_ = (config && config['filledColor']) ||
        FILLED_PIXEL_COLOR;
    this.emptyPixelColor_ = (config && config['emptyColor']) ||
        EMPTY_PIXEL_COLOR;
    this.bgColor_ = (config && config['bgColor']) ||
        BG_COLOR;

    // Set a default empty value
    if (this.getValue() === null) {
      this.setValue(this.getEmptyArray_());
    }

    /**
     * Array holding info needed to unbind events.
     * Used for disposing.
     * Ex: [[node, name, func], [node, name, func]].
     * @type {!Array<!Array<?>>}
     * @private
     */
    this.boundEvents_ = [];

    /** References to UI elements */
    this.editorPixels_ = null;
    this.fieldGroup_ = null;
    this.blockDisplayPixels_ = null;

    /** Stateful variables */
    this.mouseIsDown_ = false;
    this.valToPaintWith_ = undefined;
  }

  /**
   * Constructs a FieldBitmap from a JSON arg object.
   * @param {!Object} options A JSON object with options.
   * @return {!FieldBitmap} The new field instance.
   * @package
   * @nocollapse
   */
  static fromJson(options) {
    return new FieldBitmap(options && options['value'], undefined, options);
  }

  /**
   * Returns the width of the image in pixels.
   * @return {number} The width in pixels.
   */
  getImageWidth() {
    return this.imgWidth_;
  }

  /**
   * Returns the height of the image in pixels.
   * @return {number} The height in pixels.
   */
  getImageHeight() {
    return this.imgHeight_;
  }

  /**
   * 返回生成的字符串
   * @param {*} fieldElement field元素
   * @return {string} 返回生成的字符串
   */
  toXml(fieldElement) {
    fieldElement.textContent = JSON.stringify(this.getValue());
    return fieldElement;
  }

  /**
   * Validates that a new value meets the requirements for a valid bitmap array.
   * @param {*} newValue The new value to be tested.
   * @return {Object} The new value if it's valid, or null.
   */
  doClassValidation_(newValue = undefined) {
    if (!newValue) {
      return null;
    }
    let value;
    if (typeof newValue === 'string') {
      try {
        value = JSON.parse(newValue);
      } catch (error) {
        return null;
      }
    } else if (Array.isArray(newValue)) {
      value = newValue;
    } else {
      return null;
    }
    const newHeight = value.length;
    // The empty list is not an acceptable bitmap
    if (newHeight == 0) {
      return null;
    }

    // Check that the width matches the existing width of the image if it
    // already has a value
    const newWidth = value[0].length;
    for (const row of value) {
      if (!Array.isArray(row)) {
        return null;
      }
      if (row.length !== newWidth) {
        return null;
      }
    }

    // Check if all contents of the arrays are either 0 or 1
    for (const row of value) {
      for (const cell of row) {
        if (cell !== 0 && cell !== 1) {
          return null;
        }
      }
    }
    return value;
  }

  /**
   * Called when a new value has been validated and is about to be set.
   * @param {*} newValue The value that's about to be set.
   */
  doValueUpdate_(newValue) {
    super.doValueUpdate_(newValue);
    if (newValue) {
      const newHeight = newValue.length;
      const newWidth = newValue[0] ? newValue[0].length : 0;
      if (this.imgHeight_ !== newHeight || this.imgWidth_ !== newWidth) {
        this.imgHeight_ = newHeight;
        this.imgWidth_ = newWidth;
      }

      this.imgHeight_ = newValue.length;
      this.imgWidth_ = newValue[0] ? newValue[0].length : 0;
    }
  }

  /**
   * Show the bitmap editor dialog.
   * @param {!Event=} e Optional mouse event that triggered the field to
   *     open, or undefined if triggered programmatically.
   * @param {boolean=} _quietInput Quiet input.
   * @protected
   */
  showEditor_(e = undefined, _quietInput = undefined) {
    const editor = this.dropdownCreate_();
    Blockly.DropDownDiv.getContentDiv().appendChild(editor);
    Blockly.DropDownDiv.setColour(
        this.sourceBlock_.style.colourPrimary,
        this.sourceBlock_.style.colourTertiary);
    Blockly.DropDownDiv.showPositionedByField(
        this,
        this.dropdownDispose_.bind(this)
    );
  }

  /**
   * Updates the block display and editor dropdown when the field re-renders.
   * @protected
   * @override
   */
  render_() {
    super.render_();

    if (!this.getValue()) {
      return;
    }

    if (this.blockDisplayPixels_) {
      this.forAllCells_((r, c) => {
        const pixel = this.getValue()[r][c];

        if (this.blockDisplayPixels_) {
          this.blockDisplayPixels_[r][c].style.fill = pixel ?
            this.filledPixelColor_ :
            this.emptyPixelColor_;
        }
        if (this.editorPixels_) {
          this.editorPixels_[r][c].style.background = pixel ?
            this.filledPixelColor_ :
            this.emptyPixelColor_;
        }
      });
    }
  }

  /**
   * Determines whether the field is editable.
   * @return {boolean} True since it is always editable.
   */
  updateEditable() {
    return true;
  }

  /**
   * Creates the bitmap editor and add event listeners.
   * @return {!Element} The newly created dropdown menu.
   * @private
   */
  dropdownCreate_() {
    const dropdownEditor = this.createElementWithClassname_(
        'div',
        'dropdownEditor'
    );
    const pixelContainer = this.createElementWithClassname_(
        'div',
        'pixelContainer'
    );
    dropdownEditor.appendChild(pixelContainer);

    this.bindEvent_(dropdownEditor, 'mouseup', this.onMouseUp_);
    this.bindEvent_(dropdownEditor, 'mouseleave', this.onMouseUp_);
    this.bindEvent_(dropdownEditor, 'dragstart', (e) => {
      e.preventDefault();
    });

    this.editorPixels_ = [];
    for (let r = 0; r < this.imgHeight_; r++) {
      this.editorPixels_.push([]);
      const rowDiv = this.createElementWithClassname_('div', 'pixelRow');
      for (let c = 0; c < this.imgWidth_; c++) {
        // Add the button to the UI and save a reference to it
        const button = this.createElementWithClassname_('div', 'pixelButton');
        this.editorPixels_[r].push(button);
        rowDiv.appendChild(button);

        // Load the current pixel color
        const isOn = this.getValue()[r][c];
        button.style.background = isOn ? this.filledPixelColor_ :
            this.emptyPixelColor_;

        // Handle clicking a pixel
        this.bindEvent_(button, 'mousedown', () => {
          this.onMouseDownInPixel_(r, c);
          return true;
        });

        // Handle dragging into a pixel when mouse is down
        this.bindEvent_(button, 'mouseenter', () => {
          this.onMouseEnterPixel_(r, c);
        });
      }
      pixelContainer.appendChild(rowDiv);
    }
    const btnContainer = this.createElementWithClassname_(
        'div',
        'btnContainer'
    );
    // Add control buttons below the pixel grid
    this.addControlButton_(btnContainer,
        Blockly?.Msg?.RANDOM ?? '随机', this.randomizePixels_);
    this.addControlButton_(btnContainer,
        Blockly?.Msg?.CLEAR ?? '清空', this.clearPixels_);
    dropdownEditor.appendChild(btnContainer);
    if (this.blockDisplayPixels_) {
      this.forAllCells_((r, c) => {
        const pixel = this.getValue()[r][c];

        // if (this.blockDisplayPixels_) {
        //   this.blockDisplayPixels_[r][c].style.fill =
        //     pixel ? this.filledPixelColor_ : this.emptyPixelColor_;
        // }
        if (this.editorPixels_) {
          this.editorPixels_[r][c].style.background = pixel ?
            this.filledPixelColor_ :
            this.emptyPixelColor_;
        }
      });
    }

    return dropdownEditor;
  }

  /**
   * Initializes the on-block display.
   * @override
   */
  initView() {
    /* this.blocklyPath_ = Blockly.utils.dom.createSvgElement(
        Blockly.utils.Svg.PATH, {'class': 'blocklyPath'},
        this.fieldGroup_
    ); */
    this.createBorderRect_();
    if (this.borderRect_) {
      this.borderRect_.setAttribute('fill', this.bgColor_);
      this.borderRect_.setAttribute('rx', '0');
      this.borderRect_.setAttribute('ry', '0');
    }
    this.blockDisplayPixels_ = [];
    for (let r = 0; r < this.imgHeight_; r++) {
      const row = [];
      for (let c = 0; c < this.imgWidth_; c++) {
        /* let x;
        let y;
        let width;
        let height;
        if (r === 0) {
          y = 0;
          height = PIXEL_SIZE - 0.25;
        } else if (r === this.imgHeight_ - 1) {
          y = r * PIXEL_SIZE + 0.25;
          height = PIXEL_SIZE - 0.25;
        } else {
          y = r * PIXEL_SIZE + 0.25;
          height = PIXEL_SIZE - 0.5;
        }
        if (c === 0) {
          x = 0;
          width = PIXEL_SIZE - 0.25;
        } else if (c === this.imgWidth_ - 1) {
          x = c * PIXEL_SIZE + 0.25;
          width = PIXEL_SIZE - 0.25;
        } else {
          x = c * PIXEL_SIZE + 0.25;
          width = ;
        } */
        const square = Blockly.utils.dom.createSvgElement(
            'rect',
            {
              x: c * PIXEL_SIZE + 0.5,
              y: r * PIXEL_SIZE + 0.5,
              width: PIXEL_SIZE - 1,
              height: PIXEL_SIZE - 1,
              fill: this.emptyPixelColor_,
              fill_opacity: 1,
            },
            this.fieldGroup_
        );
        row.push(square);
      }
      this.blockDisplayPixels_.push(row);
    }
  }

  /**
   * Updates the size of the block based on the size of the underlying image.
   * @override
   * @protected
   */
  updateSize_() {
    const width = PIXEL_SIZE * this.imgWidth_;
    const height = PIXEL_SIZE * this.imgHeight_;
    /* if (this.blocklyPath_) {
      this.blocklyPath_.setAttribute('d',
          `M 0 0 L ${width} 0 L ${width} ${height} L 0 ${height} L 0 0`
      );
    } */
    if (this.borderRect_) {
      this.borderRect_.setAttribute('width', String(width));
      this.borderRect_.setAttribute('height', String(height));
    }
    this.size_.width = width;
    this.size_.height = height;
  }

  /**
   *Create control button.
   * @param {!HTMLElement} parent Parent HTML element to which
   * control button will be added.
   * @param {string} buttonText Text of the control button.
   * @param {Function} onClick Callback that will be
   * attached to the control button.
   */
  addControlButton_(parent, buttonText, onClick) {
    const button = this.createElementWithClassname_(
        'button',
        'controlButton layui-btn layui-btn-normal layui-btn-sm'
    );
    button.innerHTML = buttonText;
    parent.appendChild(button);
    this.bindEvent_(button, 'click', onClick);
  }

  /**
   * Disposes of events belonging to the bitmap editor.
   * @private
   */
  dropdownDispose_() {
    for (const event of this.boundEvents_) {
      event.length && Blockly.browserEvents.unbind(event);
    }
    this.editorPixels_ = null;
  }

  /**
   * Constructs an array of zeros with the specified width and height.
   * @return {!Array<!Array<number>>}The new value.
   */
  getEmptyArray_() {
    const newVal = [];
    for (let r = 0; r < this.imgHeight_; r++) {
      newVal.push([]);
      for (let c = 0; c < this.imgWidth_; c++) {
        newVal[r].push(0);
      }
    }
    return newVal;
  }

  /**
   * Called when a mousedown event occurs within the bounds of a pixel.
   * @private
   * @param {number} r Row number of grid.
   * @param {number} c Column number of grid.
   */
  onMouseDownInPixel_(r, c) {
    // Toggle that pixel to the opposite of its value
    const newPixelValue = 1 - this.getValue()[r][c];
    this.setPixel_(r, c, newPixelValue);
    this.mouseIsDown_ = true;
    this.valToPaintWith_ = newPixelValue;
  }

  /**
   * Called when the mouse drags over a pixel in the editor.
   * @private
   * @param {number} r Row number of grid.
   * @param {number} c Column number of grid.
   */
  onMouseEnterPixel_(r, c) {
    if (!this.mouseIsDown_) {
      return;
    }
    if (this.getValue()[r][c] !== this.valToPaintWith_) {
      this.setPixel_(r, c, this.valToPaintWith_);
    }
  }

  /**
   * Resets mouse state (e.g. After either a mouseup event or if the mouse
   * leaves the editor area).
   * @private
   */
  onMouseUp_() {
    this.mouseIsDown_ = false;
    this.valToPaintWith_ = undefined;
  }

  /**
   * Sets all the pixels in the image to a random value.
   * @private
   */
  randomizePixels_() {
    const getRandBinary = () => Math.floor(Math.random() * 2);
    const newVal = this.getEmptyArray_();
    this.forAllCells_((r, c) => {
      newVal[r][c] = getRandBinary();
    });
    this.setValue(newVal);
  }

  /**
   * Sets all the pixels to 0.
   * @private
   */
  clearPixels_() {
    const newVal = this.getEmptyArray_();
    this.forAllCells_((r, c) => {
      newVal[r][c] = 0;
    });
    this.setValue(newVal);
  }

  /**
   * Sets the value of a particular pixel.
   * @param {number} r Row number of grid.
   * @param {number} c Column number of grid.
   * @param {number} newValue Value of the pixel.
   * @private
   */
  setPixel_(r, c, newValue) {
    const newGrid = JSON.parse(JSON.stringify(this.getValue()));
    newGrid[r][c] = newValue;
    this.setValue(newGrid);
  }

  /**
   * Calls a given function for all cells in the image, with the cell
   * coordinates as the arguments.
   * @param {*} func A function to be applied.
   */
  forAllCells_(func) {
    for (let r = 0; r < this.imgHeight_; r++) {
      for (let c = 0; c < this.imgWidth_; c++) {
        func(r, c);
      }
    }
  }

  /**
   * Creates a new element with the specified type and class.
   * @param {string} elementType Type of html element.
   * @param {string} className ClassName of html element.
   * @return {!HTMLElement} The created element.
   */
  createElementWithClassname_(elementType, className) {
    const newElt = document.createElement(elementType);
    newElt.className = className;
    return newElt;
  }

  /**
   * Binds an event listener to the specified element.
   * @param {!HTMLElement} element Specified element.
   * @param {string} eventName Name of the event to bind.
   * @param {Function} callback Function to be called on specified event.
   */
  bindEvent_(element, eventName, callback) {
    this.boundEvents_.push(
        Blockly.browserEvents.
            conditionalBind(element, eventName, this, callback)
    );
  }
}

FieldBitmap.prototype.SERIALIZABLE = true;

Object.defineProperty(Blockly, 'FieldBitmap', {
  value: FieldBitmap,
  writable: true,
  enumerable: true,
  configurable: true,
});

Blockly.fieldRegistry.register('field_bitmap', Blockly.FieldBitmap);

/**
 * CSS for bitmap field.
 */
Blockly.Css.register([
  /* eslint-disable indent */
  `.dropdownEditor {
      align-items: center;
      flex-direction: column;
      display: flex;
      justify-content: center;
    }
  .pixelContainer {
    margin: 5px;
  }
  .pixelRow {
    display: flex;
    flex-direction: row;
    padding: 0;
    margin: 0;
    height: ${PIXEL_SIZE}
  }
  .pixelButton {
    width: ${PIXEL_SIZE}px;
    height: ${PIXEL_SIZE}px;
    border: 1px solid ${BG_COLOR};
  }
  .pixelDisplay {
    white-space:pre-wrap;
  }
  .controlButton {
    margin: 5px 0;
  }
  .btnContainer {
    display: flex;
    flex-direction: row;
    align-items: center;
  }
  `,
  /* eslint-enable indent */
]);
